/*
    日志落地模块的实现：
    1. 抽象落地基类
    2. 派生子类(根据不同的落地方向进行派生)
    3. 使用工厂模式进行创建与表示的分离(便于扩展)
*/

#ifndef __M_SINK_H__
#define __M_SINK_H__

#include <memory>
#include <fstream>
#include <sstream>
#include <cassert>
#include <sys/time.h>

#include "util.hpp"

namespace superlog
{
    class LogSink
    {
    public:
        using ptr = std::shared_ptr<LogSink>;
        LogSink() {}
        virtual ~LogSink() {} //用户在进行扩展时，析构可能会有一些额外的释放操作
        virtual void log(const char* data, size_t len) = 0; //将指定的数据落地到指定地址
    };

    // 落地方向：标准输出
    class StdoutSink : public LogSink
    {
    public:
        // 将日志消息写入到标准输出
        void log(const char* data, size_t len) override
        {
            // 这里不能直接使用<<流写入操作，因为该操作无法指定大小，且是以'\0'结尾，但日志输出不一定是字符串
            std::cout.write(data, len); //从data位置开始，写入len长度的数据
        }
    };

    // 落地方向：指定文件
    class FileSink : public LogSink
    {
    public:
        // 构造时传入文件名，并打开文件，将操作句柄管理起来
        FileSink(const std::string &pathname)
            : _pathname(pathname)
        {
            // 传入路径可能不存在
            // 1.创建日志文件所在目录
            util::File::createDirectory(util::File::path(_pathname)); //根据路径创建目录

            // 2.创建并打开日志文件
            // 以二进制的方式打开文件，ofstream默认是以写的方式打开，但这个日志文件写的时候应该要每次追加到文件末尾
            _ofs.open(_pathname, std::ios::binary | std::ios::app);
            assert(_ofs.is_open()); //文件没有打开，说明文件创建失败，则程序直接退出
        }

        //const std::string &pathname() { return _pathname; } // 返回文件名(完整路径)

        void log(const char* data, size_t len) override
        {
            _ofs.write(data, len); //直接通过句柄进行日志输出，不需要每次打开文件写入一条消息，然后又关闭文件
            assert(_ofs.good()); //检查当前操作句柄是否正常
        }
    private:
        std::string _pathname; //根据文件路径来写入到对应文件
        std::ofstream _ofs; //输出文件的操作句柄
    };

    // 落地方向：滚动文件(以大小进行滚动)
    class RollBySizeSink : public LogSink
    {
    public:
        // 构造时传入文件名，并打开文件，将操作句柄管理起来
        RollBySizeSink(const std::string& basename, size_t max_size)
            : _basename(basename),
              _max_fsize(max_size),
              _cur_fsize(0),
              _name_count(0)
        {
            // basename只是基本文件名(路径)，所以需要先创建出实际的输出文件名(路径)
            std::string pathname = createNewPath();

            // 创建出的路径名对应的路径可能不存在
            // 1.创建日志文件所在目录
            util::File::createDirectory(util::File::path(pathname)); //根据路径创建目录

            // 2.创建并打开日志文件
            // 二进制的形式，ofstream默认以写的方式打开，但这个日志文件写的时候应该要每次追加到文件末尾
            _ofs.open(pathname, std::ios::binary | std::ios::app);
            assert(_ofs.is_open()); //文件没有打开，说明文件创建失败，则程序直接退出
        }
        
        // 将日志消息写入到文件 —— 写入前判断文件大小，超过了最大大小就要切换文件
        void log(const char* data, size_t len)
        {
            // 当前文件大小 >= 指定最大文件大小，就需要进行切换
            if(_cur_fsize >= _max_fsize)
            {
                _ofs.close(); //先关闭原来的已经打开的文件，避免造成资源泄露
                std::string pathname = createNewPath(); // 创建新的路径
                // 这里不需要再创建日志文件所在目录，因为已经确定存在其目录
                _ofs.open(pathname, std::ios::binary | std::ios::app);
                assert(_ofs.is_open());
                // 新建文件后需要将文件大小清零，否则_cur_fsize记录的是所有文件的大小总和，而不是当前文件大小，可能会导致一直只对同一个文件进行写入 
                _cur_fsize = 0;
            }
            _ofs.write(data, len);
            assert(_ofs.good());
            _cur_fsize += len;
        }
    private:
        std::string createNewPath() //进行大小判断，超过指定大小则创建新文件路径
        {
            // 获取系统时间，以时间来构造文件名扩展名
            time_t t = util::Date::now();
            struct tm lt;
            localtime_r(&t, &lt); //将时间戳t转换成时间结构lt
            // 因为lt的成员都是整型，而_basename是字符串，类型不一致，所以这里需要使用字符串流来进行输入
            std::stringstream Path;
            Path << _basename;
            Path << lt.tm_year + 1900; //获取到的时间戳是从1900年1月1日00:00:00开始计时的
            Path << lt.tm_mon + 1; //它这里的月是默认是从0开始计算的，但实际是从1月开始算起
            Path << lt.tm_mday;
            Path << lt.tm_hour;
            Path << lt.tm_min;
            Path << lt.tm_sec;
            Path << "-";
            Path << _name_count++; //添加一个计数器，防止在1s内生成的文件较多，文件名都相同，无法做出区分
            Path << ".log";
            return Path.str(); //Path是一个stringstream，所以需要通过str()接口来获取其stirng对象
        }
    private:
        // 基础文件名+扩展文件名(以时间生成，直观，一般不会1s就生成1G的文件)组成一个实际的当前输出文件名
        std::string _basename; // ./logs/base- ——> ./logs/base-20240809183936-0.log
        std::ofstream _ofs;
        size_t _name_count; //名称计数器
        size_t _max_fsize; //记录最大值，如果当前文件超过这个值就要切换文件
        size_t _cur_fsize; //如果每次写入前都要获取文件当前文件大小的属性，效率很低，所以记录当前文件已经写入的数据大小
    };

    // 日志落地工厂，生成不同类型的日志落地方向对象
    class SinkFactory
    {
    public:
        // 采用函数模板(可以用作静态函数，传递对应的日志落地类型，因为传递参数数量不同，所以采用不定参函数，这里不能用类模板)
        template<typename SinkType, typename ...Args>
        static LogSink::ptr create(Args &&...args) //右值引用搭配完美转发
        {
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
    };
}

#endif